package com.eesujie.catimor.context;

import com.eesujie.catimor.annotation.Import;
import com.eesujie.catimor.annotation.Inject;
import com.eesujie.catimor.annotation.Provide;
import com.eesujie.catimor.exception.CreateInstanceException;
import com.eesujie.catimor.exception.ImportException;
import com.eesujie.catimor.exception.ReflectionException;
import com.eesujie.catimor.util.ReflectionUtils;
import org.jetbrains.annotations.NotNull;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.*;

class InternalInjector implements Injector {
    private final Map<Class<?>, Object> instances = new HashMap<>();
    private final Map<Class<?>, Class<?>> typeBinder = new HashMap<>();

    private final Set<Class<?>> dependencies = new HashSet<>();
    private InternalInjector(){}
    @Override
    @NotNull
    @SuppressWarnings("unchecked")
    public <T> T getInstance(@NotNull Class<T> type) {
        var realType = typeBinder.getOrDefault(type, type);
        var instance = instances.get(realType);
        if (instance == null) {
            instance = createInstance(realType);
            instances.put(realType, instance);
        }
        return (T)instance;
    }

    @NotNull
    private <T> T createInstance(@NotNull Class<T> type) {
        if (!(ReflectionUtils.deepContainsAnnotations(type, Provide.class)
                || Provider.class.isAssignableFrom(type)))
            throw new CreateInstanceException(
                    String.format("class: %s should be marked by annotation: %s or implement interface %s",
                            type.getName(),
                            Provide.class.getName(),
                            Provider.class.getName()));

        if (!dependencies.add(type)) throw new CreateInstanceException(
                String.format("Circular dependency detected when creating instance %s", type.getName()));

        var constructor = ReflectionUtils.getSuitableConstructor(type);

        try {
            return ReflectionUtils.deepContainsAnnotations(constructor, Inject.class)
                    ? createInstanceByConstructorInject(type, constructor)
                    : createInstanceByFiledInject(type, type.getDeclaredFields());
        } catch (Exception e) {
            throw new CreateInstanceException(e);
        }
    }


    @NotNull
    private <T> T createInstanceByFiledInject(@NotNull Class<T> type, @NotNull Field[] fields) throws IllegalAccessException {
        var instance = ReflectionUtils.getInstance(type);
        for (var field : fields) {
            if (!ReflectionUtils.deepContainsAnnotations(field, Inject.class)) continue;
            field.setAccessible(true);
            field.set(instance, getInstance(field.getType()));
        }
        return instance;
    }

    @NotNull
    private <T> T createInstanceByConstructorInject(@NotNull Class<T> type, @NotNull Constructor<?> constructor) {
        var parameterTypes = constructor.getParameterTypes();
        List<Object> args = new ArrayList<>(parameterTypes.length);
        for (var parameterType : parameterTypes) {
            args.add(getInstance(parameterType));
        }
        return ReflectionUtils.getInstance(type, args.toArray());
    }

    @NotNull
    static InternalInjector build(@NotNull Iterable<Class<? extends InjectorConfig>> configTypes) {
        var internal = new InternalInjector();
        for (var configType : configTypes) {
            var config = ReflectionUtils.getInstance(configType);
            try {
                var configMethod = configType.getDeclaredMethod("config");
                configMethod.setAccessible(true);
                configMethod.invoke(config);
            } catch (Exception e) {
                throw new ReflectionException(e);
            }
            internal.typeBinder.putAll(config.typeBinder);
            var importAnnotation = configType.getAnnotation(Import.class);
            if (importAnnotation == null) continue;
            var providerTypes = importAnnotation.value();
            for (var providerType : providerTypes) {
                var provider = internal.getInstance(providerType);
                var methods = providerType.getDeclaredMethods();
                for (var method : methods) {
                    if (!ReflectionUtils.deepContainsAnnotations(method, Provide.class)) continue;
                    try {
                        var instance = method.invoke(provider);
                        var instanceType = method.getReturnType();
                        internal.instances.put(instanceType, instance);
                    } catch (Exception e) {
                        throw new ImportException(e);
                    }
                }
            }
        }
        return internal;
    }
}
